package peace.transport.json {
import flash.utils.describeType;

/**
 * Чтение и запись в json <br>
 *
 * Для простоты маппинга объектов данных сервера на клиент
 * достаточно расширить JsonObject и определить нужные свойства.<br><br>
 *
 * Для чтения массива используется JsonObject.jsonToArray (json:String):Array
 *
 * @author peace
 *
 */
dynamic public class JsonObject extends Object {
    /**
     * Вытаскиваем строчку до ближайшей запятой,
     * с учетом что в строчке могут быть группы [],{},""
     * с запятыми или такими-же группами внутри
     */
    protected static const BEFORE_NEXT_COMMA:RegExp = /(\[(?1)*?\]|\{(?1)*?\}|\".*?\"|.)*?\,/gims;

    /**
     *
     * @param json
     *
     */
    public function JsonObject(json:String = null) {
        if (json) read(json);
    }

    /**
     *
     * Читаем JsonObject из json-строки
     *
     * @param s
     *
     */
    public function read(s:String):void {
        s = trim(s);

        if (s.charAt(0) != "{") {
            if (s != '') trace("JsonObject::read - строка не json-объект!");
            return;
        }

        // отрезаем {}
        s = s.substring(1, s.length - 1);

        // для работы регулярки BEFORE_NEXT_COMMA
        s += ",";
        // массив пар "key:value"
        var pairs:Array = s.match(BEFORE_NEXT_COMMA);

        var index:int;
        var key:String, value:String;
        var first:String;
        for each (s in pairs) {
            index = s.indexOf(":");
            if (index == -1) continue;

            key = trim(s.substring(0, index));
            // отрезаем "", ибо ключи в JSON в кавычках
            key = key.substring(1, key.length - 1);

            value = trim(s.substring(index + 1,
                    s.length - 1)); // отрезуем лишнюю "," после регулярки

            first = value.charAt(0);
            if (first == "{") {
                this[key] = new JsonObject(value);
            }
            else if (first == "[") {
                this[key] = jsonToArray(value);
            }
            else {
                this[key] = readSimple(value);
            }

        }

    }

    /**
     * Пишем в json-строку
     * @return
     */
    public function toJson():String {
        var key:String, value:*;
        var keys:Array = [];
        if (this.constructor == JsonObject) {
            for (key in this) keys.push(key);
        } else {
            var vars:XMLList = describeType(this).variable.@name;
            for each (key in vars) keys.push(key.toString())
        }

        var pair:String, pairs:Array = [];
        for each (key in keys) {
            // ключи в JSON в кавычках
            pair = "\"" + key + "\":";
            value = this[key];

            if (value is Array) {
                pair += arrayToJson(value as Array);
            }
            else if (value == null || value == undefined) {
                pair += "null";
            }
            else if (typeof value == "object") {
                pair += objectToJson(value);
            }
            else {
                pair += writeSimple(value);
            }

            pairs.push(pair);
        }

        return "{" + pairs.join(",") + "}";
    }

    /**
     *
     * Читаем json-массив
     *
     * @param json String
     * @param jsonObjectItemClass Class для истанцирования элементов массива в кастомные JsonObject
     * @return
     *
     */
    public static function jsonToArray(json:String, jsonObjectItemClass:Class = null):Array {
        // отрезаем []
        json = json.substring(1, json.length - 1);
        // не паримся с пустым массивом
        if (trim(json) == '') return [];

        // для работы регулярки BEFORE_NEXT_COMMA
        json += ",";
        // массив элементов
        var items:Array = json.match(BEFORE_NEXT_COMMA);

        var result:Array = [];
        var value:String, first:String;
        var i:int;
        for (i = 0; i < items.length; i++) {
            value = items[i];
            value = value.substring(0,
                    value.length - 1); //отрезуем лишнюю "," после регулярки
            value = trim(value);

            first = value.charAt(0);
            if (first == "{") {
                var clazz:Class = jsonObjectItemClass || JsonObject;
                result[i] = new clazz(value);
            }
            else if (first == "[") {
                result[i] = jsonToArray(value);
            }
            else {
                result[i] = readSimple(value);
            }
        }
        return result;
    }

    /**
     *
     * @param a
     * @return
     *
     */
    public static function arrayToJson(a:Array):String {
        a = a.slice(); // нельзя менять исходный массив
        var item:*, i:int;
        for (i = 0; i < a.length; i++) {
            item = a[i];
            if (item is Array) {
                a[i] = arrayToJson(item as Array);
            }
            else if (item == null) {
                a[i] = "null";
            }
            else if (item == undefined) {
                a[i] = "";
            }
            else if (typeof item == "object") {
                a[i] = objectToJson(item);
            }
            else {
                a[i] = writeSimple(item);
            }
        }

        return "[" + a.join(",") + "]";
    }

    /**
     *
     * @param s
     * @return
     *
     */
    protected static function readSimple(s:String):* {
        if (["\"", "'"].indexOf(s.charAt(0)) > -1) {
            return s.substring(1, s.length - 1);
        }
        else if (s == "true" || s == "false") {
            return Boolean(s == "true");
        }
        else if (s == "null") {
            return null;
        }
        else {
            return isNaN(Number(s)) ? null : Number(s);
        }
    }

    /**
     *
     * @param value
     * @return
     *
     */
    protected static function writeSimple(value:*):String {
        if (typeof value == "string") {
            return "\"" + value + "\"";
        }
        return String(value);
    }

    /**
     * Отрезаем whitespaces в начале и конце
     *
     * @param s
     * @return
     */
    protected static function trim(s:String):String {
        return s.replace(/^\s+|\s+$/g, "");
    }

    /**
     * Конвертация произвольного объекта в json
     *
     * @param o
     * @return
     */
    public static function objectToJson(o:Object):String {
        if (o is JsonObject) return JsonObject(o).toJson();

        var jo:JsonObject = new JsonObject();
        var s:String;
        for (s in o) {
            jo[s] = o[s];
        }
        return jo.toJson();
    }
}
}